﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using LockCrypt.Core;
using Lime49;

namespace LockCrypt.DAL {
    /// <summary>
    /// Parses a Keepass 2.x XML file.
    /// </summary>
    public class Keepass2xParser : IReadOnlyDatabase {
        private readonly string _fileName;
        private readonly ILogProvider _logger;
        private XElement _dummyElement = new XElement("dummy");

        /// <summary>
        /// Initializes a new <see cref="Keepass2xParser"/>.
        /// </summary>
        /// <param name="fileName">Name of the file to parse.</param>
        /// <param name="logger">The logger (can be <c>null</c>).</param>
        public Keepass2xParser(string fileName, ILogProvider logger) {
            _fileName = fileName;
            _logger = logger == null ? new ConsoleLogger() : logger;
        }

        /// <summary>
        /// Attempts to load the XML document without parsing it's contents.
        /// </summary>
        /// <returns><c>true</c> if the file can be opened, otherwise false.</returns>
        public bool CanLoadContents() {
            try {
                string rawXml = File.ReadAllText(_fileName, Encoding.UTF8);
                XDocument doc = XDocument.Parse(rawXml, LoadOptions.None);
                return (doc.Element("KeePassFile") ?? _dummyElement).Element("Root") != null; // if it has a valid root, it may be valid.
            } catch {
                return false;
            }
        }

        /// <summary>
        /// Reads the whole contents of the file.
        /// </summary>
        /// <returns>The whole contents of the file.</returns>
        public DatabaseContents ReadDatabase() {
            if(!File.Exists(_fileName))
                throw new LockCryptException("File does not exist", 232);
            string rawXml = File.ReadAllText(_fileName, Encoding.UTF8);
            XDocument doc = XDocument.Parse(rawXml, LoadOptions.None);
            DatabaseContents contents = new DatabaseContents();
            XElement xRoot = (doc.Element("KeePassFile") ?? _dummyElement).Element("Root");
            if(xRoot == null)
                throw new LockCryptException("Invalid Keepass 2.x Database", 233);

            IEnumerable<XElement> rootGroups = xRoot.Elements("Group");
            foreach(var rootGroup in rootGroups) {
                try {
                    IGroup group = ParseGroup(rootGroup);
                    IEnumerable<IGroup> groupIncludingSubs = group.Flatten();
                    IEnumerable<IAccount> descendantAccounts = groupIncludingSubs.SelectMany(g => g.Accounts);
                    contents.Accounts.AddRange(descendantAccounts);
                    contents.Groups.AddRange(groupIncludingSubs);
                } catch(Exception ex) {
                    _logger.Log(string.Format("Invalid group: {0}", ex.Message), true);
                }
            }
            return contents;
        }

        /// <summary>
        /// Parses a group, it's descendant groups and child accounts.
        /// </summary>
        /// <param name="xGroup">The group.</param>
        /// <returns>A group</returns>
        private IGroup ParseGroup(XElement xGroup) {
            IEnumerable<XElement> groupElements = xGroup.Elements("Group");
            IGroup group = new GroupBase() {
                Name = HtmlUtils.HtmlDecode(xGroup.Element("Name").Value)
            };
            int sortIndex = 0;
            foreach(var groupElement in groupElements) {
                try {
                    IGroup childGroup = ParseGroup(groupElement);
                    childGroup.ParentID = group.ID;
                    childGroup.SortIndex = sortIndex++;
                    group.Groups.Add(childGroup);
                } catch(Exception ex) {
                    _logger.Log(string.Format("Invalid group: {0}", ex.Message), true);
                }
            }

            IEnumerable<XElement> accountElements = xGroup.Elements("Entry");
            foreach(var accountElement in accountElements) {
                try {
                    IAccount acc = new AccountBase() {
                        Name = "New Account", // overriden, but just incase
                        Created = DateTime.Parse(xGroup.Element("Times").Element("CreationTime").Value),
                        Modified = DateTime.Parse(xGroup.Element("Times").Element("LastModificationTime").Value),
                    };
                    acc.ParentID = group.ID;
                    IEnumerable<XElement> fieldElements = accountElement.Elements("String");
                    foreach(var fieldElement in fieldElements) {
                        try {
                            string fieldName = HtmlUtils.HtmlDecode(fieldElement.Element("Key").Value);
                            string fieldValue = HtmlUtils.HtmlDecode(fieldElement.Element("Value").Value);
                            if(fieldName.Equals("notes", StringComparison.InvariantCultureIgnoreCase)) {
                                acc.Notes = fieldValue;
                            } else if(fieldName.Equals("title", StringComparison.InvariantCultureIgnoreCase)) {
                                acc.Name = fieldValue;
                            } else {
                                IField field = new FieldBase() {
                                    Name = fieldName,
                                    Value = fieldValue,
                                    SortIndex = sortIndex++
                                };
                                field.Meta = FieldBase.GuessMeta(field);
                                acc.Fields.Add(field);
                            }
                        } catch(Exception ex) {
                            _logger.Log(string.Format("Error parsing field for account '{0}: {1}", acc.Name, ex.Message), true);
                        }
                    }
                    _logger.Log(string.Format("Parsed account '{0}", acc.Name), StatusMessage.StatusMessageType.Success);
                    group.Accounts.Add(acc);
                } catch(Exception ex) {
                    _logger.Log("Error parsing account: " + ex.Message, true);
                }
            }
            return group;
        }
    }
}
